﻿using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Xml.Linq;
using Devonline.Core;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Logging;

namespace Devonline.CloudService.Tencent.Weixin;

/// <summary>
/// 微信公众平台接入接口
/// </summary>
/// <param name="logger"></param>
/// <param name="endpoint">微信服务器接口配置</param>
/// <param name="httpContextAccessor"></param>
/// <param name="cache"></param>
/// <param name="httpClientFactory">http client 工厂, 需要在全局配置微信服务器接口地址</param>
public class WeixinService(
    ILogger<WeixinService> logger,
    IWeixinEndpoint endpoint,
    IHttpContextAccessor httpContextAccessor,
    IDistributedCache cache,
    IHttpClientFactory httpClientFactory
    ) : IWeixinService
{
    private readonly ILogger<WeixinService> _logger = logger;
    private readonly IDistributedCache _cache = cache;
    private readonly IWeixinEndpoint _endpoint = endpoint;
    private readonly IHttpClientFactory _httpClientFactory = httpClientFactory;
    private readonly HttpContext _httpContext = httpContextAccessor.HttpContext!;
    private readonly HttpRequest _request = httpContextAccessor.HttpContext!.Request;

    /// <summary>
    /// 模板消息接口
    /// </summary>
    private const string _templateMessagePath = "cgi-bin/message/template/send";

    /// <summary>
    /// 微信服务器认证
    /// 用于开始交互前, 微信服务器调用应用服务器, 用以验证服务器配置有效
    /// docs: https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Access_Overview.html
    /// </summary>
    /// <param name="signature">微信加密签名，signature结合了开发者填写的token参数和请求中的timestamp参数、nonce参数。</param>
    /// <param name="timestamp">时间戳</param>
    /// <param name="nonce">随机数</param>
    /// <param name="echostr">随机字符串</param>
    /// <returns></returns>
    public virtual string Authenticate(string signature, string timestamp, string nonce, string echostr)
    {
        //string? signature = _request.GetRequestOption<string>(nameof(signature));
        //string? timestamp = _request.GetRequestOption<string>(nameof(timestamp));
        //string? nonce = _request.GetRequestOption<string>(nameof(nonce));
        //string? echostr = _request.GetRequestOption<string>(nameof(echostr));
        //if (string.IsNullOrWhiteSpace(signature) || string.IsNullOrWhiteSpace(timestamp) || string.IsNullOrWhiteSpace(nonce) || string.IsNullOrWhiteSpace(echostr))
        //{
        //    throw new UnauthorizedAccessException(nameof(signature));
        //}

        ArgumentException.ThrowIfNullOrWhiteSpace(_endpoint.Token);
        _logger.LogDebug($"即将进行微信服务器认证, nonce: {nonce}, echostr: {echostr}");
        var signArray = new string[] { _endpoint.Token, timestamp, nonce }.OrderBy(x => x);
        var signString = string.Join(string.Empty, signArray);
        var signResult = SHA1.Create().GetHashValue(signString).ToHexString(char.MinValue);
        var result = signResult.Equals(signature, StringComparison.InvariantCultureIgnoreCase);

        _logger.LogInformation("微信服务器认证" + (result ? "完成" : "失败, 签名不符!"));
        return result ? echostr : throw new UnauthorizedAccessException();
    }
    /// <summary>
    /// 微信登录
    /// docs:
    /// app: https://developers.weixin.qq.com/doc/oplatform/Mobile_App/WeChat_Login/Development_Guide.html
    /// web: https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Wechat_Login.html
    /// </summary>
    /// <param name="code"></param>
    /// <param name="state"></param>
    /// <returns></returns>
    /// <exception cref="UnauthorizedAccessException"></exception>
    public virtual async Task AuthorizeAsync(string code, string? state = null)
    {
        _logger.LogInformation("已收到微信用户授权回调请求!");
        var accessToken = await GetAccessTokenAsync(code);
        var userInfo = await GetUserInfoAsync(accessToken.AccessToken, accessToken.OpenId) ?? throw new UnauthorizedAccessException("未能获取微信用户信息!");
        var claims = new List<Claim>();
        var propertyInfos = typeof(WeixinUserInfo).GetProperties();
        var prefix = CookieAuthenticationDefaults.AuthenticationScheme + AppSettings.CHAR_COLON;
        foreach (var propertyInfo in propertyInfos)
        {
            var value = propertyInfo.GetValue(userInfo)?.ToString();
            if (!string.IsNullOrWhiteSpace(value))
            {
                claims.Add(new Claim(prefix + propertyInfo.GetJsonPropertyName(), value));
            }
        }

        claims.Add(new Claim(prefix + AppSettings.CLAIM_TYPE_JWT_SUBJECT, userInfo.OpenId!));
        claims.Add(new Claim(prefix + AppSettings.CLAIM_TYPE_OPEN_ID, userInfo.OpenId!));
        var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity(claims));
        await _httpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, claimsPrincipal);
        _httpContext.User = claimsPrincipal;
    }

    /// <summary>
    /// 获取 access token
    /// docs:
    /// 公众号: https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Get_access_token.html
    /// 小程序: https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/mp-access-token/getAccessToken.html
    /// </summary>
    /// <returns></returns>
    public virtual async Task<WeixinAccessToken> GetAccessTokenAsync(string? code = default)
    {
        var cacheKey = AppSettings.CACHE_APPLICATION + nameof(WeixinAccessToken.AccessToken) + AppSettings.CHAR_UNDERLINE + _endpoint.AppId;
        var accessToken = await _cache.GetValueAsync<WeixinAccessToken>(cacheKey);
        if (accessToken is not null)
        {
            return accessToken;
        }

        _logger.LogInformation("未能从缓存中获取到 access token 或 access token 已过期, 将重新获取!");

        var url = string.IsNullOrWhiteSpace(code) ? $"/cgi-bin/token?appid={_endpoint.AppId}&secret={_endpoint.Secret}&grant_type=client_credential" : $"/sns/oauth2/access_token?appid={_endpoint.AppId}&secret={_endpoint.Secret}&grant_type=authorization_code&code={code}";
        using var httpClient = _httpClientFactory.CreateClient(nameof(WeixinService));
        var response = await httpClient.GetStringAsync(url) ?? throw new Exception("获取访问令牌失败!");

        try
        {
            GetWeixinResponse(response);
            accessToken = response.ToJsonObject<WeixinAccessToken>();
            if (accessToken is not null && !string.IsNullOrWhiteSpace(accessToken.AccessToken) && accessToken.ExpiresIn > 0)
            {
                _logger.LogDebug("access token 获取成功!");
                await _cache.SetValueAsync(cacheKey, accessToken, new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(accessToken.ExpiresIn) });
                return accessToken;
            }

            throw new Exception("获取访问令牌失败, 服务器返回详情: " + response);
        }
        catch (Exception ex)
        {
            throw new Exception("获取访问令牌失败, 错误信息: " + ex.GetMessage(), ex);
        }
    }
    /// <summary>
    /// 获取稳定版 access token
    /// docs: 
    /// 公众号: https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/getStableAccessToken.html
    /// 小程序: https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/mp-access-token/getStableAccessToken.html
    /// </summary>
    /// <param name="force">
    /// 是否强制获取, 默认使用 false。
    /// 1. force_refresh = false 时为普通调用模式，access_token 有效期内重复调用该接口不会更新 access_token；
    /// 2. 当force_refresh = true 时为强制刷新模式，会导致上次获取的 access_token 失效，并返回新的 access_token
    /// </param>
    /// <returns></returns>
    public virtual async Task<WeixinAccessToken> GetStableAccessTokenAsync(bool? force = false)
    {
        var cacheKey = AppSettings.CACHE_APPLICATION + nameof(WeixinAccessToken.AccessToken) + AppSettings.CHAR_UNDERLINE + _endpoint.AppId;
        var accessToken = await _cache.GetValueAsync<WeixinAccessToken>(cacheKey);
        if (accessToken is not null)
        {
            return accessToken;
        }

        _logger.LogInformation("未能从缓存中获取到 access token 或 access token 已过期, 将重新获取!");

        using var httpClient = _httpClientFactory.CreateClient(nameof(WeixinService));
        var url = $"/cgi-bin/stable_token";
        using var httpContent = new StringContent(new WeixinStableToken { AppId = _endpoint.AppId!, Secret = _endpoint.Secret!, ForceRefresh = false }.ToJsonString(), new MediaTypeHeaderValue(ContentType.Txt));
        using var httpResponse = await httpClient.PostAsync(url, httpContent);
        var response = await httpResponse.Content.ReadAsStringAsync();
        if (!httpResponse.IsSuccessStatusCode)
        {
            throw new Exception($"获取稳定版访问令牌失败, 服务器返回状态: {httpResponse.StatusCode}, 服务器返回详情: " + response);
        }

        try
        {
            GetWeixinResponse(response);
            accessToken = response.ToJsonObject<WeixinAccessToken>();
            if (accessToken is not null && !string.IsNullOrWhiteSpace(accessToken.AccessToken) && accessToken.ExpiresIn > 0)
            {
                _logger.LogDebug("access token 获取成功!");
                await _cache.SetValueAsync(cacheKey, accessToken, new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(accessToken.ExpiresIn) });
                return accessToken;
            }

            throw new Exception("获取稳定版访问令牌失败, 服务器返回详情: " + response);
        }
        catch (Exception ex)
        {
            throw new Exception("获取稳定版访问令牌失败, 错误信息: " + ex.GetMessage(), ex);
        }
    }
    /// <summary>
    /// 获取刷新 token
    /// </summary>
    /// <returns></returns>
    /// <exception cref="NotImplementedException"></exception>
    public virtual Task<WeixinAccessToken> GetRefreshTokenAsync()
    {
        throw new NotImplementedException();
    }

    /// <summary>
    /// 获取用户信息
    /// 微信公众平台需要先从客户端提交用户信息到服务器, 服务器端进行查找后, 在返回客户端
    /// 微信开放平台调用会直接从微信服务器端获取用户信息, 并进行查找后, 在返回客户端
    /// </summary>
    /// <param name="accessToken">access token</param>
    /// <param name="openId">微信用户的 openid</param>
    /// <returns></returns>
    public virtual async Task<WeixinUserInfo?> GetUserInfoAsync(string accessToken, string openId)
    {
        var url = $"sns/userinfo?access_token={accessToken}&openid={openId}";
        using var httpClient = _httpClientFactory.CreateClient(nameof(WeixinService));
        var response = await httpClient.GetStringAsync(url) ?? throw new Exception("获取微信用户信息失败!");

        try
        {
            GetWeixinResponse(response);
            return response.ToJsonObject<WeixinUserInfo>() ?? throw new Exception("微信用户信息获取失败, 服务器返回详情: " + response);
        }
        catch (Exception ex)
        {
            throw new Exception("微信用户信息获取失败, 错误信息: " + ex.GetMessage(), ex);
        }
    }
    /// <summary>
    /// 接受微信消息
    /// </summary>
    /// <returns></returns>
    /// <exception cref="NotImplementedException"></exception>
    public virtual async Task<WeixinMessage?> OnReceiveMessageAsync()
    {
        _logger.LogDebug("收到来自微信的消息!");
        using var stream = new StreamReader(_request.Body);
        var xml = await stream.ReadToEndAsync();
        if (string.IsNullOrWhiteSpace(xml))
        {
            _logger.LogWarning("微信消息内容为空!");
            return null;
        }

        _logger.LogInformation("收到来自微信的消息: " + Environment.NewLine + xml);
        var doc = XDocument.Parse(xml);
        var nodes = doc.Descendants();
        var typeNode = nodes.FirstOrDefault(x => x.Name == nameof(WeixinMessage.MsgType));
        var messageType = string.IsNullOrWhiteSpace(typeNode?.Value) ? MessageType.Text : Enum.Parse<MessageType>(typeNode.Value, true);

        WeixinMessage message = messageType switch
        {
            MessageType.Image => new ImageMessage(),
            MessageType.Event => new EventMessage(),
            _ => new TextMessage()
        };

        _logger.LogDebug($"已读取 msgType 节点值为: {typeNode?.Value}, 解析为枚举值: {messageType}, 已创建类型为: {message.MsgType.GetDisplayName()} 的微信消息!");
        foreach (var node in nodes)
        {
            if (node != typeNode)
            {
                message.SetPropertyValue(node.Name.LocalName, node.Value, true);
            }
        }

        message.CreateTime = DateTime.SpecifyKind(message.CreateTime, DateTimeKind.Utc);

        _logger.LogDebug("收到来自微信的消息解析后的内容为: " + message.ToJsonString());
        return message;
    }
    /// <summary>
    /// 发送模板消息
    /// 模板消息接口的 http request body 仅支持 StringContent 且为: text/plain 类型
    /// </summary>
    /// <param name="message">模板消息</param>
    /// <returns></returns>
    public virtual async Task SendTemplateMessageAsync(TemplateMessage message)
    {
        var messageTemplate = _endpoint.MessageTemplates.FirstOrDefault(x => x.Name.Equals(message.TemplateName, StringComparison.InvariantCultureIgnoreCase));
        if (messageTemplate is null)
        {
            _logger.LogError("尚未配置消息模板: " + message.TemplateName);
            return;
        }

        _logger.LogInformation($"将使用消息模板 {message.TemplateName} 向 {message.OpenId} 发送模板消息: {message.Id}");
        messageTemplate = messageTemplate.Copy();
        var templateMessage = new TemplateMessageRequest
        {
            ToUser = message.OpenId!,
            TemplateId = messageTemplate.Id,
            ClientMessageId = message.Id,
            Data = []
        };

        if (!string.IsNullOrWhiteSpace(message.Url))
        {
            //使用 damain 配置项作为主域名地址
            templateMessage.Url = _endpoint.Domain + message.Url;
        }

        _logger.LogDebug("将根据配置项, 构造模板消息参数!");
        var attributes = message.GetType().GetFieldAttributes();
        if (!attributes.Any())
        {
            _logger.LogError($"消息模板: {message.TemplateName} 使用的数据对象模型配置有误!");
            return;
        }

        foreach (var attribute in attributes)
        {
            var value = attribute.Property.GetValue(message);
            if (value is not null && value is IConvertible convertibleValue)
            {
                templateMessage.Data.Add(attribute.Name, new TemplateMessageParam { Value = (convertibleValue is DateTime dateTime) ? dateTime.ToString(attribute.Format) : (convertibleValue.To<string>() ?? string.Empty) });
            }
        }

        _logger.LogDebug("将发起模板消息调用请求!");
        using var httpClient = _httpClientFactory.CreateClient(nameof(WeixinService));
        var accessToken = await GetAccessTokenAsync();
        var url = $"{_templateMessagePath}?access_token={accessToken}";
        using var httpContent = new StringContent(templateMessage.ToJsonString(), new MediaTypeHeaderValue(ContentType.Txt));
        using var httpResponse = await httpClient.PostAsync(url, httpContent);
        if (httpResponse.IsSuccessStatusCode)
        {
            var response = await httpResponse.Content.ReadFromJsonAsync<WeixinResponse>() ?? throw new Exception($"使用消息模板 {message.TemplateName} 向 {message.OpenId} 发送模板消息: {message.Id} 返回值转换失败!");
            if (response.ErrorCode == AppSettings.UNIT_ZERO)
            {
                _logger.LogInformation($"使用消息模板 {message.TemplateName} 向 {message.OpenId} 发送模板消息: {message.Id} 成功!");
                return;
            }

            throw new Exception($"使用消息模板 {message.TemplateName} 向 {message.OpenId} 发送模板消息: {message.Id} 失败, 错误码: {response.ErrorCode}, 错误描述: {response.ErrorMessage}");
        }

        throw new Exception($"使用消息模板 {message.TemplateName} 向 {message.OpenId} 发送模板消息: {message.Id} 请求失败, HTTP 状态码: {httpResponse.StatusCode}, 错误详情: " + await httpResponse.Content.ReadAsStringAsync());
    }

    /// <summary>
    /// 从服务器响应获取 WeixinResponse, 无法转换时抛出异常
    /// </summary>
    /// <param name="response"></param>
    /// <returns></returns>
    /// <exception cref="Exception"></exception>
    private WeixinResponse GetWeixinResponse(string response)
    {
        var weixinResponse = response.ToJsonObject<WeixinResponse>() ?? throw new Exception("获取微信返回值失败, 服务器返回详情: " + response); ;
        if (weixinResponse.ErrorCode != AppSettings.UNIT_ZERO)
        {
            throw new Exception($"获取微信返回值失败, 服务器反馈: error code {weixinResponse.ErrorCode}, error message: {weixinResponse.ErrorMessage}");
        }

        return weixinResponse;
    }
}